/*
 * Ganesa Copyright (C) 2008 Wim Crols <twimpiex@gnx.be>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License Version 3
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package be.gnx.ganesa.application.ui.widget;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Hashtable;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;

import be.gnx.ganesa.application.model.Cuboid;

public abstract class SlippyMapBoundingBoxTabItemControl extends BoundingBoxTabItemControl {

  private Canvas slippyMapCanvas;

  private SlippyMapPanAndZoom slippyMapPanAndZoom;

  private int slippyMapWidth;

  private int slippyMapHeight;

  private int minimumZoomLevel;

  private int maximumZoomLevel = 17;

  private int currentZoomLevel = 0;

  private int numberOfXTiles;

  private int numberOfYTiles;

  private int startFromXTile = 0;

  private int startFromYTile = 0;

  private int xTileOffset = 0;

  private int yTileOffset = 0;

  private Image tile[][];

  boolean mouseDown = false;

  private int previousMouseX = 0;

  private int previousMouseY = 0;

  private Hashtable<Integer, Hashtable<Integer, Hashtable<Integer, Image>>> tileCache = new Hashtable<Integer, Hashtable<Integer,Hashtable<Integer,Image>>>();

  public SlippyMapBoundingBoxTabItemControl(Composite parent, int style) {
    super(parent, style);
    slippyMapCanvas = new Canvas(this, SWT.NONE);
    slippyMapPanAndZoom = new SlippyMapPanAndZoom(slippyMapCanvas, SWT.VERTICAL);
    resize();

    slippyMapPanAndZoom.addSelectionListener(new SelectionListener() {
      public void widgetDefaultSelected(SelectionEvent selectionEvent) {
        widgetSelected(selectionEvent);
      }
      public void widgetSelected(SelectionEvent selectionEvent) {
        currentZoomLevel = slippyMapPanAndZoom.getCurrentZoomLevel();
        resize();
      }
    });

    slippyMapCanvas.addPaintListener(new PaintListener() {
      public void paintControl(PaintEvent paintEvent) {
        drawTiles(paintEvent.gc);
      }
    });
    slippyMapCanvas.addMouseMoveListener(new MouseMoveListener() {
      public void mouseMove(MouseEvent event) {
        if (mouseDown) {
          xTileOffset = xTileOffset + (previousMouseX - event.x);
          yTileOffset = yTileOffset + (previousMouseY - event.y);
          previousMouseX = event.x;
          previousMouseY = event.y;

          boolean loadNewTiles = false;

          if (xTileOffset > 255) {
            if ((startFromXTile + numberOfXTiles + 1) < (1 << currentZoomLevel)) {
              xTileOffset -= 256;
              startFromXTile++;
              loadNewTiles = true;
            } else {
              // TODO
              xTileOffset = 255;
            }
          }
          if (yTileOffset > 255) {
            if ((startFromYTile + numberOfYTiles + 1) < (1 << currentZoomLevel)) {
              yTileOffset -= 256;
              startFromYTile++;
              loadNewTiles = true;
            } else {
              // TODO
              yTileOffset = 255;
            }
          }

          if (xTileOffset < 0) {
            if (startFromXTile > 0) {
              xTileOffset += 256;
              startFromXTile--;
              loadNewTiles = true;
            } else {
              // TODO
              xTileOffset = 0;
            }
          }
          if (yTileOffset < 0) {
            if (startFromYTile > 0) {
              yTileOffset += 256;
              startFromYTile--;
              loadNewTiles = true;
            } else {
              // TODO
              yTileOffset = 0;
            }
          }

          if (loadNewTiles) {
            loadTiles();
          }
          GC gc = new GC(slippyMapCanvas);
          drawTiles(gc);
          gc.dispose();
        }
      }
    });
    slippyMapCanvas.addMouseListener(new MouseListener() {
      public void mouseUp(MouseEvent event) {
        if (mouseDown) {
          mouseDown = false;
          previousMouseX = 0;
          previousMouseY = 0;
        }
      }
      public void mouseDoubleClick(MouseEvent e) {
        // TODO Auto-generated method stub
        
      }
      public void mouseDown(MouseEvent event) {
        mouseDown = true;
        previousMouseX = event.x;
        previousMouseY = event.y;
      }
    });
    slippyMapCanvas.addMouseWheelListener(new MouseWheelListener() {
      public void mouseScrolled(MouseEvent event) {
        if (event.count > 0) {
          if (currentZoomLevel < maximumZoomLevel) {
            currentZoomLevel++;

            startFromXTile *= 2;
            xTileOffset = (xTileOffset + event.x) * 2;
            while (xTileOffset > 255) {
              startFromXTile++;
              xTileOffset -= 256;
            }

            startFromYTile *= 2;
            yTileOffset = (yTileOffset + event.y) * 2;
            while (yTileOffset > 255) {
              startFromYTile++;
              yTileOffset -= 256;
            }

            xTileOffset -= event.x;
            while (xTileOffset < 0) {
              startFromXTile--;
              xTileOffset += 256;
            }

            yTileOffset -= event.y;
            while (yTileOffset < 0) {
              startFromYTile--;
              yTileOffset += 256;
            }

            if ((startFromXTile + numberOfXTiles) >= (1 << currentZoomLevel)) {
              startFromXTile = (1 << currentZoomLevel) - numberOfXTiles;
            }
            if ((startFromYTile + numberOfYTiles) >= (1 << currentZoomLevel)) {
              startFromYTile = (1 << currentZoomLevel) - numberOfYTiles;
            }
            if (startFromXTile < 0) {
              startFromXTile = 0;
              xTileOffset = 0;
            }
            if (startFromYTile < 0) {
              startFromYTile = 0;
              yTileOffset = 0;
            }
          }
        } else if (event.count < 0) {
          if (currentZoomLevel > minimumZoomLevel) {
            currentZoomLevel--;

            startFromXTile /= 2;
            xTileOffset = (xTileOffset + event.x) / 2;
            while (xTileOffset > 255) {
              startFromXTile++;
              xTileOffset -= 256;
            }

            startFromYTile /= 2;
            yTileOffset = (yTileOffset + event.y) / 2;
            while (yTileOffset > 255) {
              startFromYTile++;
              yTileOffset -= 256;
            }

            xTileOffset -= event.x;
            while (xTileOffset < 0) {
              startFromXTile--;
              xTileOffset += 256;
            }

            yTileOffset -= event.y;
            while (yTileOffset < 0) {
              startFromYTile--;
              yTileOffset += 256;
            }

            if ((startFromXTile + numberOfXTiles) >= (1 << currentZoomLevel)) {
              startFromXTile = (1 << currentZoomLevel) - numberOfXTiles;
            }
            if ((startFromYTile + numberOfYTiles) >= (1 << currentZoomLevel)) {
              startFromYTile = (1 << currentZoomLevel) - numberOfYTiles;
            }
            if (startFromXTile < 0) {
              startFromXTile = 0;
              xTileOffset = 0;
            }
            if (startFromYTile < 0) {
              startFromYTile = 0;
              yTileOffset = 0;
            }
          }
        }
        loadTiles();
        slippyMapCanvas.redraw();
      }
    });
  }

  public void dispose() {
    return;
  }

  protected void resize() {
    slippyMapCanvas.setBounds(0, 0, slippyMapCanvas.getParent().getClientArea().width, slippyMapCanvas.getParent().getClientArea().height);
    slippyMapWidth = slippyMapCanvas.getSize().x;
    slippyMapHeight = slippyMapCanvas.getSize().y;
    numberOfXTiles = ((slippyMapWidth + 255) / 256) + 1;
    numberOfYTiles = ((slippyMapHeight + 255) / 256) + 1;
    int maxNumberOfTiles = Math.max(numberOfXTiles, numberOfYTiles);
    minimumZoomLevel = 0;
    while ((1 << minimumZoomLevel) < maxNumberOfTiles) {
      minimumZoomLevel++;
    }
    if (currentZoomLevel < minimumZoomLevel) {
      currentZoomLevel = minimumZoomLevel;
    }
    if (maximumZoomLevel < minimumZoomLevel) {
      maximumZoomLevel =  minimumZoomLevel;
    }
    if (currentZoomLevel > maximumZoomLevel) {
      currentZoomLevel = maximumZoomLevel;
    }
    slippyMapPanAndZoom.setMinimumZoomLevel(minimumZoomLevel);
    slippyMapPanAndZoom.setMaximumZoomLevel(maximumZoomLevel);
    slippyMapPanAndZoom.setCurrentZoomLevel(currentZoomLevel);
    slippyMapPanAndZoom.setToolTipText("zoom level = " + currentZoomLevel);
    //Point scaleSize = scale.computeSize(20, 100, true);
    slippyMapPanAndZoom.setBounds(10, 10, 60, 100);
    loadTiles();
  }

  private void loadTiles() {
    tile = new Image[numberOfXTiles][numberOfYTiles];
    for (int x = 0; x < numberOfXTiles; x++) {
      for (int y = 0; y < numberOfYTiles; y++) {
        tile[x][y] = getTile(currentZoomLevel, x + startFromXTile, y + startFromYTile);
      }
    }
  }

  private void drawTiles(GC gc) {
    slippyMapPanAndZoom.setCurrentZoomLevel(currentZoomLevel);
    slippyMapPanAndZoom.setToolTipText("zoom level = " + currentZoomLevel);
    if (tile != null) {
      for (int x = 0; x < numberOfXTiles; x++) {
        for (int y = 0; y < numberOfYTiles; y++) {
          if (tile[x][y] != null) {
            gc.drawImage(tile[x][y], (x * 256) - xTileOffset, (y * 256) - yTileOffset);
          }
        }
      }
    }
    if (currentZoomLevel > 3) {
      BigDecimal minLongitude = tileXToLongitude(currentZoomLevel, startFromXTile, 8);
      BigDecimal minLatitude = tileYToLatitude(currentZoomLevel, startFromYTile + numberOfYTiles, 8);
      BigDecimal maxLongitude = tileXToLongitude(currentZoomLevel, startFromXTile + numberOfXTiles, 8);
      BigDecimal maxLatitude = tileYToLatitude(currentZoomLevel, startFromYTile, 8);
      String boundingBoxString = minLongitude.toString();
      boundingBoxString += ", " + minLatitude;
      boundingBoxString += "\t" + maxLongitude;
      boundingBoxString += ", " + maxLatitude;
      if ((maxLongitude.subtract(minLongitude).compareTo(BigDecimal.valueOf(1, 1)) < 0) && (maxLatitude.subtract(minLatitude).compareTo(BigDecimal.valueOf(1, 1)) < 0)) {
        boundingBox = new Cuboid(Math.toRadians(minLatitude.doubleValue()), Math.toRadians(minLongitude.doubleValue()), 0, Math.toRadians(maxLatitude.subtract(minLatitude).doubleValue()), Math.toRadians(maxLongitude.subtract(minLongitude).doubleValue()), 0);
        boundingBoxString += " OK!";
      } else {
        boundingBox = null;
      }
      gc.drawText(boundingBoxString, 0, 0, true);
    } else {
      boundingBox = null;
    }
  }

  private Image getTile(int zoomLevel, int x, int y) {
    while (x < 0) {
      x += (1 << zoomLevel);
    }
    while (y < 0) {
      y += (1 << zoomLevel);
    }
    x %= (1 << zoomLevel);
    y %= (1 << zoomLevel);

    Image tile = getTileFromCache(zoomLevel, x, y);

    if (tile == null) {
      if ((zoomLevel > 3) && (getTileXCache(zoomLevel - 1, x / 2).get(y / 2) != null)) {
        tile = new Image(slippyMapCanvas.getDisplay(), new Rectangle(0, 0, 256, 256));
        GC gc = new GC(tile);
        gc.drawImage(getTileXCache(zoomLevel - 1, x / 2).get(y / 2), (x % 2) * 128, (y % 2) * 128, 128, 128, 0, 0, 256, 256);
        gc.dispose(); 
      } else {
        tile = new Image(slippyMapCanvas.getDisplay(), new Rectangle(0, 0, 256, 256));
        GC gc = new GC(tile);
        gc.setForeground(slippyMapCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK));
        gc.drawLine(0, 0, 0, 256);
        gc.drawLine(0, 256, 256, 256);
        gc.drawLine(256, 256, 0, 256);
        gc.drawLine(0, 0, 256, 0);
        gc.drawLine(0, 0, 256, 256);
        gc.drawLine(0, 256, 256, 0);
        gc.dispose(); 
      }
      getTileXCache(zoomLevel, x).put(y, tile);
      slippyMapCanvas.getDisplay().asyncExec (new LoadTile(zoomLevel, x, y, tile));
    }

    return tile;
  }

  private class LoadTile implements Runnable {
    private int zoomLevel;
    private int x;
    private int y;
    private Image oldTile;

    public LoadTile(int zoomLevel, int x, int y, Image oldTile) {
      this.zoomLevel = zoomLevel;
      this.x = x;
      this.y = y;
      this.oldTile = oldTile;
    }

    public void run() {
      while (x < 0) {
        x += (1 << zoomLevel);
      }
      while (y < 0) {
        y += (1 << zoomLevel);
      }
      x %= (1 << zoomLevel);
      y %= (1 << zoomLevel);

      Image newTile = null;
      try {
        URL url = composeTileURL(x, y, zoomLevel);
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setRequestMethod("GET");
        urlConnection.connect();
        InputStream inputStream = urlConnection.getInputStream();
        if (!slippyMapCanvas.isDisposed()) {
          newTile = new Image(slippyMapCanvas.getDisplay(), inputStream);
        }
        urlConnection.disconnect();
      } catch (IOException ioException) {
        System.err.println(ioException);
      }
      if (!oldTile.isDisposed()) {
        if (newTile != null) {
        GC gc = new GC(oldTile);
        gc.drawImage(newTile, 0, 0, 256, 256, 0, 0, 256, 256);
        gc.dispose();
        if (!slippyMapCanvas.isDisposed()) {
          slippyMapCanvas.redraw ();
        }
        }
      }
    }
  }

  private Image getTileFromCache(int zoomLevel, int x, int y) {
    return getTileYCache(zoomLevel, x, y);
  }

  private Image getTileYCache(int zoomLevel, int x, int y) {
    return getTileXCache(zoomLevel, x).get(y);
  }

  private Hashtable<Integer, Image> getTileXCache(int zoomLevel, int x) {
    if (getTileZoomCache(zoomLevel).get(x) == null) {
      getTileZoomCache(zoomLevel).put(x, new Hashtable<Integer, Image>());
    }
    return getTileZoomCache(zoomLevel).get(x);
  }

  private Hashtable<Integer, Hashtable<Integer, Image>> getTileZoomCache(int zoomLevel) {
    if (tileCache.get(zoomLevel) == null) {
      tileCache.put(zoomLevel, new Hashtable<Integer, Hashtable<Integer, Image>>());
    }
    return tileCache.get(zoomLevel);
  }

  public static int latitudeToTileY(int zoom, BigDecimal latitude) {
    if ((zoom < 3) || (zoom > 18)) {
      return -1;
    }
    double l = latitude.doubleValue() / 180 * Math.PI;
    double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
    return (int) ((1 << (zoom - 1)) * (Math.PI - pf) / Math.PI);
  }

  public static int longitudeToTileX(int zoom, BigDecimal longitude) {
    if ((zoom < 3) || (zoom > 18)) {
      return -1;
    }
    return (int) ((1 << (zoom - 3)) * (longitude.doubleValue() + 180.0) / 45.0);
  }

  public static BigDecimal tileYToLatitude(int zoom, int y, int scale) {
    if ((zoom < 3) || (zoom > 18)) {
      return new BigDecimal(Double.MIN_VALUE).setScale(scale, BigDecimal.ROUND_HALF_EVEN);
    }
    return new BigDecimal(Math.atan(Math.sinh(Math.PI - (Math.PI * y / (1 << (zoom - 1))))) * 180 / Math.PI)
        .setScale(scale, BigDecimal.ROUND_HALF_EVEN);
  }

  public static BigDecimal tileXToLongitude(int zoom, int x, int scale) {
    if ((zoom < 3) || (zoom > 18)) {
      return new BigDecimal(Double.MIN_VALUE).setScale(scale, BigDecimal.ROUND_HALF_EVEN);
    }
    return new BigDecimal(x * 45.0 / (1 << (zoom - 3)) - 180.0).setScale(scale, BigDecimal.ROUND_HALF_EVEN);
  }

  protected abstract URL composeTileURL(int x, int y, int zoomLevel);

}